package socket;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Scanner;

/**
 * 聊天室的服务器端
 */
public class Server {
    /*
     * java.net.ServerSocket
     * ServerSocket是运行在服务器端的,它的主要工作:
     * 1:打开服务器端口(客户端就是根据这个端口与服务器链接)
     * 2:监听该服务器端口,一旦一个客户端链接,就会返回一个Socket实例,
     * 并通过这个Socket实例与链接的客户端进行交互
     * 如果我们将Socket比喻为"电话",ServerSocket比喻为"总机"
     */
    private ServerSocket server;
    //该数组用于存放所有客户端的输出流,用于广播消息给所有的客户端
    private PrintWriter[] allOut = {};

    public Server() {
        try {
            System.out.println("正在启动服务器...");
            server = new ServerSocket(8088);
            System.out.println("服务器启动完毕!!!");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void start() {
        try {
            while (true) {
                System.out.println("等待客户端链接...");
                /*
                 * ServerSocket中提供了一个方法:
                 * Socket accept()
                 * 当程序执行到这个方法后,会阻塞,当有一个客户端访问时,
                 * 这个方法就会立即返回一个Socket对象,通过这个Scoket与该客户端进行链接
                 */
                Socket socket = server.accept();
                System.out.println("一个客户端链接了!!!");
                ClientHandler handler = new ClientHandler(socket);
                Thread t = new Thread(handler);
                t.start();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        Server server = new Server();
        server.start();
    }

    /*
     * 该线程负责与特定客户端交互
     * 内部类:可以访问外部类的任意属性
     * */
    private class ClientHandler implements Runnable {
        private Socket socket;

        //声明含参构造,负责实例化对象时,接收socket参数
        public ClientHandler(Socket socket) {
            this.socket = socket;
        }

        @Override
        public void run() {
            PrintWriter pw = null;
            try {
                /*
                 * 通过Secket的方法:
                 * InputStream getInputStream()
                 * 可以获取一个输入字节流,可以读取来自远端计算机发送过来的字节数据
                 */
                InputStream in = socket.getInputStream();
                InputStreamReader isr = new InputStreamReader(in, StandardCharsets.UTF_8);
                BufferedReader br = new BufferedReader(isr);
                //通过socket获取输出流,用于给客户端发送信息
                OutputStream out = socket.getOutputStream();
                //创建转换流,链接字节流和字符流
                OutputStreamWriter osw =
                        new OutputStreamWriter(out, StandardCharsets.UTF_8);
                //创建缓冲字符流,提高写出效率
                BufferedWriter bw = new BufferedWriter(osw);
                //按行写入字符串,自带行刷新
                pw = new PrintWriter(bw, true);
                //将该客户端的输出流pw存储到allOut数组中
                //由于数组的长度一旦创建不可改变,所以输出流数量不可知,所以每存储一个,需要进行数组扩容
                //this不行,因为this是ClientHandler.
                /*
                 * 一般情况下,同步监视器对象选取时,就选择多个线程并发操作的临界资源就可以了,allOut数组进行了扩容了,数组一旦扩容,数组就变成一个新数组了,内存地址也发生了变化,所以对于多线程而言,此时allOut已经不是唯一的,所以同步代码块失效
                 * */
                synchronized (Server.this) {//内部类指向外部类对象
                    //1.扩容数组,让allOut数组的长度+1
                    allOut = Arrays.copyOf(allOut, allOut.length + 1);
                    //2.将pw存入到共享的数组中(存储到数组中的最后一个位置)
                    allOut[allOut.length - 1] = pw;
                }
                //广播通知所有的客户端有新的客户端上线了
                sendMessage("有新的客户端上线了!当前在线人数:" + allOut.length);
                String line;
                while ((line = br.readLine()) != null) {//当读取的内容是null,就停止读取
                    System.out.println("客户端说:" + line);
                    //将消息回复给所有客户端
                    sendMessage(line);//这个方法等效于下面注释的代码
                    //allOut.fori
//                    for (int i = 0; i < allOut.length; i++) {
//                        allOut[i].println(line);
//                    }
                }
            } catch (IOException e) {
                e.printStackTrace();
            } finally {//不论客户端是正常断开还是异常断开,都只要执行取出输出流操作
                synchronized (Server.this) {
                    //遍历allOut数组 allOut.fori
                    for (int i = 0; i < allOut.length; i++) {
                        //找到要删除的元素(利用内存地址是否相同判断)
                        if (allOut[i] == pw) {
                            //将最后一个元素替换到目标删除元素
                            allOut[i] = allOut[allOut.length - 1];
                            //将数组进行缩容
                            allOut = Arrays.copyOf(allOut, allOut.length - 1);
                            //由于上面的代码执行了,说明已经将下线的客户端的对应服务器的输出流取出了,就不需要再次执行for循环,因为后面的元素一定不是要删除的元素,所以可以直接利用break关键字停止循环,可以有效地提升效率
                            break;
                        }
                    }
                }
                sendMessage("有客户端下线了!当前在线人数:" + allOut.length);
                //服务器端也关闭电话
                try {
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        /*
         * 广播消息给所有的客户端(可以发客户端给服务器发送的内容,也可以发送在线人数)
         *   send 发送 Message 消息
         * */
        private void sendMessage(String msg) {
            synchronized (Server.this){
                for (int i = 0; i < allOut.length; i++) {
                    allOut[i].println(msg);
                }
            }
        }
    }
}
